#! /bin/sh

set -e

pkidir='@PKIDIR@'
command=
prev=
force=no
batch=no
log='@LOGDIR@/ofp-pki.log'
keytype=rsa
bits=2048
for option; do
    # This option-parsing mechanism borrowed from a Autoconf-generated
    # configure script under the following license:

    # Copyright (C) 1992, 1993, 1994, 1995, 1996, 1998, 1999, 2000, 2001,
    # 2002, 2003, 2004, 2005, 2006 Free Software Foundation, Inc.
    # This configure script is free software; the Free Software Foundation
    # gives unlimited permission to copy, distribute and modify it.

    # If the previous option needs an argument, assign it.
    if test -n "$prev"; then
        eval $prev=\$option
        prev=
        continue
    fi
    case $option in
        *=*) optarg=`expr "X$option" : '[^=]*=\(.*\)'` ;;
        *) optarg=yes ;;
    esac

    case $dashdash$option in
        --)
            dashdash=yes ;;
        -h|--help)
            cat <<EOF
ofp-pki, for managing a simple OpenFlow public key infrastructure 
usage: $0 [OPTION...] COMMAND [ARG...]

The valid stand-alone commands and their arguments are:
  init                 Initialize the PKI
  req NAME             Create new private key and certificate request
                       named NAME-privkey.pem and NAME-req.pem, resp.
  sign NAME [TYPE]     Sign switch certificate request NAME-req.pem,
                       producing certificate NAME-cert.pem
  req+sign NAME [TYPE] Combine the above two steps, producing all three files.
  verify NAME [TYPE]   Checks that NAME-cert.pem is a valid TYPE certificate
  fingerprint FILE     Prints the fingerprint for FILE
  self-sign NAME       Sign NAME-req.pem with NAME-privkey.pem,
                       producing self-signed certificate NAME-cert.pem

The following additional commands manage an online PKI:
  ls [PREFIX] [TYPE]   Lists incoming requests of the given TYPE, optionally 
                       limited to those whose fingerprint begins with PREFIX
  flush [TYPE]         Rejects all incoming requests of the given TYPE
  reject PREFIX [TYPE] Rejects the incoming request(s) whose fingerprint begins
                       with PREFIX and has the given TYPE
  approve PREFIX [TYPE] Approves the incoming request whose fingerprint begins
                       with PREFIX and has the given TYPE
  expire [AGE]         Rejects all incoming requests older than AGE, in
                       one of the forms Ns, Nmin, Nh, Nday (default: 1day)
  prompt [TYPE]        Interactively prompts to accept or reject each incoming
                       request of the given TYPE

Each TYPE above is a certificate type: 'switch' (default) or 'controller'.

Options for 'init', 'req', and 'req+sign' only:
  -k, --key=rsa|dsa    Type of keys to use (default: rsa)
  -B, --bits=NBITS     Number of bits in keys (default: 2048).  For DSA keys,
                         this has an effect only on 'init'.
  -D, --dsaparam=FILE  File with DSA parameters (DSA only)
                         (default: dsaparam.pem within PKI directory)
Options for use with the 'sign' and 'approve' commands:
  -b, --batch          Skip fingerprint verification
Options that apply to any command:
  -d, --dir=DIR        Directory where the PKI is located
                         (default: $pkidir)
  -f, --force          Continue even if file or directory already exists
  -l, --log=FILE       Log openssl output to FILE (default: ofp-log.log)
  -h, --help           Print this usage message.
EOF
            exit 0
            ;;
        --di*=*)
            pkidir=$optarg
            ;;
        --di*|-d)
            prev=pkidir
            ;;
        --k*=*)
            keytype=$optarg
            ;;
        --k*|-k)
            prev=keytype
            ;;
        --bi*=*)
            bits=$optarg
            ;;
        --bi*|-B)
            prev=bits
            ;;
        --ds*=*)
            dsaparam=$optarg
            ;;
        --ds*|-D)
            prev=dsaparam
            ;;
        --l*=*)
            log=$optarg
            ;;
        --l*|-l)
            prev=log
            ;;
        --force|-f)
            force=yes
            ;;
        --ba*|-b)
            batch=yes
            ;;
        -*)
            echo "unrecognized option $option" >&2
            exit 1
            ;;
        *)
            if test -z "$command"; then
                command=$option
            elif test -z "${arg1+set}"; then
                arg1=$option
            elif test -z "${arg2+set}"; then
                arg2=$option
            else
                echo "$option: only two arguments may be specified" >&2
                exit 1
            fi
            ;;
    esac
    shift
done
if test -n "$prev"; then
    option=--`echo $prev | sed 's/_/-/g'`
    { echo "$as_me: error: missing argument to $option" >&2
        { (exit 1); exit 1; }; }
fi
if test -z "$command"; then
    echo "$0: missing command name; use --help for help" >&2
    exit 1
fi
if test "$keytype" != rsa && test "$keytype" != dsa; then
    echo "$0: argument to -k or --key must be rsa or dsa"
    exit 1
fi
if test "$bits" -lt 1024; then
    echo "$0: argument to -B or --bits must be at least 1024"
    exit 1
fi
if test -z "$dsaparam"; then
    dsaparam=$pkidir/dsaparam.pem
fi
case $log in
    /*) ;;
    *) $log="$PWD/$log" ;;
esac

if test "$command" = "init"; then
    if test -e "$pkidir" && test "$force" != "yes"; then
        echo "$0: $pkidir already exists and --force not specified" >&2
        exit 1
    fi

    if test ! -d "$pkidir"; then
        mkdir -p "$pkidir"
    fi
    cd "$pkidir"
    exec 3>>$log

    if test $keytype = dsa && test ! -e dsaparam.pem; then
        echo "Generating DSA parameters, please wait..." >&2
        openssl dsaparam -out dsaparam.pem $bits 1>&3 2>&3
    fi

    # Create the CAs.
    for ca in controllerca switchca; do
        echo "Creating $ca..." >&2
        oldpwd=$PWD
        mkdir -p $ca
        cd $ca

        mkdir -p certs crl newcerts
        mkdir -p -m 0700 private
        mkdir -p -m 0733 incoming
        touch index.txt
        test -e crlnumber || echo 01 > crlnumber
        test -e serial || echo 01 > serial

        # Put DSA parameters in directory.
        if test $keytype = dsa && test ! -e dsaparam.pem; then
            cp ../dsaparam.pem .
        fi

    # Write CA configuration file.
        if test ! -e ca.cnf; then
            sed "s/@ca@/$ca/g" > ca.cnf <<'EOF'
[ req ]
prompt = no
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
C = US
ST = CA
L = Palo Alto
O = OpenFlow
OU = @ca@
CN = OpenFlow @ca@ CA Certificate

[ ca ]
default_ca = the_ca

[ the_ca ]
dir            = .                     # top dir
database       = $dir/index.txt        # index file.
new_certs_dir  = $dir/newcerts         # new certs dir
certificate    = $dir/cacert.pem       # The CA cert
serial         = $dir/serial           # serial no file
private_key    = $dir/private/cakey.pem# CA private key
RANDFILE       = $dir/private/.rand    # random number file
default_days   = 365                   # how long to certify for
default_crl_days= 30                   # how long before next CRL
default_md     = md5                   # md to use
policy         = policy                # default policy
email_in_dn    = no                    # Don't add the email into cert DN
name_opt       = ca_default            # Subject name display option
cert_opt       = ca_default            # Certificate display option
copy_extensions = none                 # Don't copy extensions from request

# For the CA policy
[ policy ]
countryName             = optional
stateOrProvinceName     = optional
organizationName        = match
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional
EOF
        fi

        # Create certificate authority.
        if test $keytype = dsa; then
            newkey=dsa:dsaparam.pem
        else
            newkey=rsa:$bits
        fi
        openssl req -config ca.cnf -nodes \
            -newkey $newkey -keyout private/cakey.pem -out careq.pem \
            1>&3 2>&3
        openssl ca -config ca.cnf -create_serial -out cacert.pem \
            -days 1095 -batch -keyfile private/cakey.pem -selfsign \
            -infiles careq.pem 1>&3 2>&3
        chmod 0700 private/cakey.pem

        cd "$oldpwd"
    done
    exit 0
fi

one_arg() {
    if test -z "$arg1" || test -n "$arg2"; then
        echo "$0: $command must have exactly one argument; use --help for help" >&2
        exit 1
    fi
}

zero_or_one_args() {
    if test -n "$arg2"; then
        echo "$0: $command must have zero or one arguments; use --help for help" >&2
        exit 1
    fi
}

one_or_two_args() {
    if test -z "$arg1"; then
        echo "$0: $command must have one or two arguments; use --help for help" >&2
        exit 1
    fi
}

must_not_exist() {
    if test -e "$1" && test "$force" != "yes"; then
        echo "$0: $1 already exists and --force not supplied" >&2
        exit 1
    fi
}

resolve_prefix() {
    test -n "$type" || exit 123 # Forgot to call check_type?

    case $1 in
        ????*)
            ;;
        *)
            echo "Prefix $arg1 is too short (less than 4 hex digits)"
            exit 0
            ;;
    esac
    
    fingerprint=$(cd "$pkidir/${type}ca/incoming" && echo "$1"*-req.pem | sed 's/-req\.pem$//')
    case $fingerprint in
        "${1}*")
            echo "No certificate requests matching $1"
            exit 1
            ;;
        *" "*)
            echo "$1 matches more than one certificate request:"
            echo $fingerprint | sed 's/ /\
/g'
            exit 1
            ;;
        *)
            # Nothing to do.
            ;;
    esac
    req="$pkidir/${type}ca/incoming/$fingerprint-req.pem"
    cert="$pkidir/${type}ca/certs/$fingerprint-cert.pem"
}

make_tmpdir() {
    TMP=/tmp/ofp-pki.tmp$$
    rm -rf $TMP
    trap "rm -rf $TMP" 0
    mkdir -m 0700 $TMP
}

fingerprint() {
    local file=$1
    local name=${1-$2}
    local date=$(date -r $file)
    local fingerprint
    if grep -q -e '-BEGIN CERTIFICATE-' "$file"; then
        fingerprint=$(openssl x509 -noout -in "$file" -fingerprint |
                      sed 's/SHA1 Fingerprint=//' | tr -d ':')
    else
        fingerprint=$(sha1sum "$file" | awk '{print $1}')
    fi
    printf "$name\\t$date\\n"
    case $file in
        $fingerprint*)
            printf "\\t(correct fingerprint in filename)\\n"
            ;;
        *)
            printf "\\tfingerprint $fingerprint\\n"
            ;;
    esac
}

verify_fingerprint() {
    fingerprint "$@"
    if test $batch != yes; then
        echo "Does fingerprint match? (yes/no)"
        read answer
        if test "$answer" != yes; then 
            echo "Match failure, aborting" >&2
            exit 1
        fi
    fi
}

check_type() {
    if test x = x"$1"; then
        type=switch
    elif test "$1" = switch || test "$1" = controller; then 
        type=$1
    else
        echo "$0: type argument must be 'switch' or 'controller'" >&2
        exit 1
    fi
}

parse_age() {
    number=$(echo $1 | sed 's/^\([0-9]\+\)\([[:alpha:]]\+\)/\1/')
    unit=$(echo $1 | sed 's/^\([0-9]\+\)\([[:alpha:]]\+\)/\2/')
    case $unit in
        s)
            factor=1
            ;;
        min)
            factor=60
            ;;
        h)
            factor=3600
            ;;
        day)
            factor=86400
            ;;
        *)
            echo "$1: age not in the form Ns, Nmin, Nh, Nday (e.g. 1day)" >&2
            exit 1
            ;;
    esac
    echo $(($number * $factor))
}

must_exist() {
    if test ! -e "$1"; then
        echo "$0: $1 does not exist" >&2
        exit 1
    fi
}

pkidir_must_exist() {
    if test ! -e "$pkidir"; then
        echo "$0: $pkidir does not exist (need to run 'init' or use '--dir'?)" >&2
        exit 1
    elif test ! -d "$pkidir"; then
        echo "$0: $pkidir is not a directory" >&2
        exit 1
    fi
}

make_request() {
    must_not_exist "$arg1-privkey.pem"
    must_not_exist "$arg1-req.pem"
    make_tmpdir
    cat > "$TMP/req.cnf" <<EOF
[ req ]
prompt = no
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
C = US
ST = CA
L = Palo Alto
O = OpenFlow
OU = OpenFlow certifier
CN = OpenFlow certificate for $arg1
EOF
    if test $keytype = rsa; then
        newkey=rsa:$bits
    else
        must_exist "$dsaparam"
        newkey=dsa:$dsaparam
    fi
    openssl req -config "$TMP/req.cnf" -text -nodes \
        -newkey $newkey -keyout "$1-privkey.pem" -out "$1-req.pem" 1>&3 2>&3
}

sign_request() {
    must_exist "$1"
    must_not_exist "$2"
    pkidir_must_exist

    (cd "$pkidir/${type}ca" && 
     openssl ca -config ca.cnf -batch -in /dev/stdin) \
        < "$1" > "$2.tmp$$" 2>&3
    mv "$2.tmp$$" "$2"
}

glob() {
    local files=$(echo $1)
    if test "$files" != "$1"; then
        echo "$files"
    fi
}

exec 3>>$log || true
if test "$command" = req; then
    one_arg

    make_request "$arg1"
    fingerprint "$arg1-req.pem"
elif test "$command" = sign; then
    one_or_two_args
    check_type "$arg2"
    verify_fingerprint "$arg1-req.pem"

    sign_request "$arg1-req.pem" "$arg2-cert.pem"
elif test "$command" = req+sign; then
    one_or_two_args
    check_type "$arg2"

    pkidir_must_exist
    make_request "$arg1"
    sign_request "$arg1-req.pem" "$arg1-cert.pem"
    fingerprint "$arg1-req.pem"
elif test "$command" = verify; then
    one_or_two_args
    must_exist "$arg1-cert.pem"
    check_type "$arg2"

    pkidir_must_exist
    openssl verify -CAfile "$pkidir/${type}ca/cacert.pem" "$arg1-cert.pem"
elif test "$command" = fingerprint; then
    one_arg

    fingerprint "$arg1"
elif test "$command" = self-sign; then
    one_arg
    must_exist "$arg1-req.pem"
    must_exist "$arg1-privkey.pem"
    must_not_exist "$arg1-cert.pem"

    openssl x509 -in "$arg1-req.pem" -out "$arg1-cert.pem" \
        -signkey "$arg1-privkey.pem" -req -text 2>&3
elif test "$command" = ls; then
    check_type "$arg2"

    cd "$pkidir/${type}ca/incoming"
    for file in $(glob "$arg1*-req.pem"); do
        fingerprint $file
    done
elif test "$command" = flush; then
    check_type "$arg1"

    rm -f "$pkidir/${type}ca/incoming/"*
elif test "$command" = reject; then
    one_or_two_args
    check_type "$arg2"
    resolve_prefix "$arg1"

    rm -f "$req"
elif test "$command" = approve; then
    one_or_two_args
    check_type "$arg2"
    resolve_prefix "$arg1"

    make_tmpdir
    cp "$req" "$TMP/$req"
    verify_fingerprint "$TMP/$req"
    sign_request "$TMP/$req"
    rm -f "$req" "$TMP/$req"
elif test "$command" = prompt; then
    zero_or_one_args
    check_type "$arg1"

    make_tmpdir
    cd "$pkidir/${type}ca/incoming"
    for req in $(glob "*-req.pem"); do
        cp "$req" "$TMP/$req"

        cert=$(echo "$pkidir/${type}ca/certs/$req" |
               sed 's/-req.pem/-cert.pem/')
        if test -f $cert; then
            echo "Request $req already approved--dropping duplicate request"
            rm -f "$req" "$TMP/$req"
            continue
        fi

        echo
        echo
        fingerprint "$TMP/$req" "$req"
        printf "Disposition for this request (skip/approve/reject)? "
        read answer
        case $answer in
            approve)
                echo "Approving $req"
                sign_request "$TMP/$req" "$cert"
                rm -f "$req" "$TMP/$req"
                ;;
            r*)
                echo "Rejecting $req"
                rm -f "$req" "$TMP/$req"
                ;;
            *)
                echo "Skipping $req"
                ;;
        esac
    done
elif test "$command" = expire; then
    zero_or_one_args
    cutoff=$(($(date +%s) - $(parse_age ${arg1-1day})))
    for type in switch controller; do
        cd "$pkidir/${type}ca/incoming" || exit 1
        for file in $(glob "*"); do
            time=$(date -r "$file" +%s)
            if test "$time" -lt "$cutoff"; then
                rm -f "$file"
            fi
        done
    done
else
    echo "$0: $command command unknown; use --help for help" >&2
    exit 1
fi
